/*
 * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

/*
 * @test
 * @bug 8143037 8142447 8144095 8140265 8144906 8146138 8147887 8147886 8148316 8148317 8143955 8157953 8080347 8154714 8166649 8167643 8170162 8172102 8165405 8174796 8174797 8175304 8167554 8180508 8166232 8196133 8199912 8211694 8223688 8254196 8295984
 * @summary Tests for Basic tests for REPL tool
 * @modules jdk.compiler/com.sun.tools.javac.api
 *          jdk.compiler/com.sun.tools.javac.main
 *          jdk.jdeps/com.sun.tools.javap
 *          jdk.jshell/jdk.internal.jshell.tool
 * @library /tools/lib
 * @build toolbox.ToolBox toolbox.JarTask toolbox.JavacTask
 * @build KullaTesting TestingInputStream Compiler
 * @run junit/timeout=600 ToolBasicTest
 * @key intermittent
 */

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Scanner;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.sun.net.httpserver.HttpServer;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.fail;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

public class ToolBasicTest extends ReplToolTesting {

    @Test
    public void elideStartUpFromList() {
        test((a) -> assertCommandOutputContains(a, "123", "==> 123"),
                (a) -> assertCommandCheckOutput(a, "/list", (s) -> {
                    int cnt;
                    try (Scanner scanner = new Scanner(s)) {
                        cnt = 0;
                        while (scanner.hasNextLine()) {
                            String line = scanner.nextLine();
                            if (!line.trim().isEmpty()) {
                                ++cnt;
                            }
                        }
                    }
                    assertEquals(1, cnt, "Expected only one listed line");
                })
        );
    }

    @Test
    public void elideStartUpFromSave() throws IOException {
        Compiler compiler = new Compiler();
        Path path = compiler.getPath("myfile");
        test(
                (a) -> assertCommandOutputContains(a, "123", "==> 123"),
                (a) -> assertCommand(a, "/save " + path.toString(), "")
        );
        try (Stream<String> lines = Files.lines(path)) {
            assertEquals(1, lines.count(), "Expected only one saved line");
        }
    }

    @Test
    public void testInterrupt() {
        ReplTest interrupt = (a) -> assertCommand(a, "\u0003", "");
        for (String s : new String[] { "", "\u0003" }) {
            test(false, new String[]{"--no-startup"},
                    (a) -> assertCommand(a, "int a = 2 +" + s, ""),
                    interrupt,
                    (a) -> assertCommand(a, "int a\u0003", ""),
                    (a) -> assertCommand(a, "int a = 2 + 2\u0003", ""),
                    (a) -> assertCommandCheckOutput(a, "/vars", assertVariables()),
                    (a) -> evaluateExpression(a, "int", "2", "2"),
                    (a) -> assertCommandCheckOutput(a, "/vars", assertVariables()),
                    (a) -> assertCommand(a, "void f() {", ""),
                    (a) -> assertCommand(a, "int q = 10;" + s, ""),
                    interrupt,
                    (a) -> assertCommand(a, "void f() {}\u0003", ""),
                    (a) -> assertCommandCheckOutput(a, "/methods", assertMethods()),
                    (a) -> assertMethod(a, "int f() { return 0; }", "()int", "f"),
                    (a) -> assertCommandCheckOutput(a, "/methods", assertMethods()),
                    (a) -> assertCommand(a, "class A {" + s, ""),
                    interrupt,
                    (a) -> assertCommand(a, "class A {}\u0003", ""),
                    (a) -> assertCommandCheckOutput(a, "/types", assertClasses()),
                    (a) -> assertClass(a, "interface A {}", "interface", "A"),
                    (a) -> assertCommandCheckOutput(a, "/types", assertClasses()),
                    (a) -> assertCommand(a, "import java.util.stream." + s, ""),
                    interrupt,
                    (a) -> assertCommand(a, "import java.util.stream.\u0003", ""),
                    (a) -> assertCommandCheckOutput(a, "/imports", assertImports()),
                    (a) -> assertImport(a, "import java.util.stream.Stream", "", "java.util.stream.Stream"),
                    (a) -> assertCommandCheckOutput(a, "/imports", assertImports())
            );
        }
    }

    @Test
    public void testCtrlD() {
        test(false, new String[]{"--no-startup"},
                a -> {
                    if (!a) {
                        closeCommandInput();
                    } else {
                        throw new IllegalStateException();
                    }
                }
        );
    }

    private final Object lock = new Object();
    private PrintWriter out;
    private boolean isStopped;
    private Thread t;
    private void assertStop(boolean after, String cmd, String output) {
        if (!after) {
            isStopped = false;
            StringWriter writer = new StringWriter();
            out = new PrintWriter(writer);
            setCommandInput(cmd + "\n");
            t = new Thread(() -> {
                int i = 1;
                int n = 30;
                synchronized (lock) {
                    do {
                        setCommandInput("\u0003");
                        if (!isStopped) {
                            out.println("Not stopped. Try again: " + i);
                            try {
                                lock.wait(1000);
                            } catch (InterruptedException ignored) {
                            }
                        }
                    } while (i++ < n && !isStopped);
                    if (!isStopped) {
                        System.err.println(writer.toString());
                        fail("Evaluation was not stopped: '" + cmd + "'");
                    }
                }
            });
            t.start();
        } else {
            synchronized (lock)  {
                out.println("Evaluation was stopped successfully: '" + cmd + "'");
                isStopped = true;
                lock.notify();
            }
            try {
                t.join();
                t = null;
            } catch (InterruptedException ignored) {
            }
            assertOutput(getCommandOutput(), "", "command");
            assertOutput(getCommandErrorOutput(), "", "command error");
            assertOutput(getUserOutput().trim(), output, "user");
            assertOutput(getUserErrorOutput(), "", "user error");
        }
    }

    @Test
    public void testStop() {
        test(
                (a) -> assertStop(a, "while (true) {}", ""),
                (a) -> assertStop(a, "while (true) { try { Thread.sleep(100); } catch (InterruptedException ex) { } }", "")
        );
    }

    @Test
    public void testRerun() {
        test(false, new String[] {"--no-startup"},
                (a) -> assertCommand(a, "/0", "|  No snippet with ID: 0"),
                (a) -> assertCommand(a, "/5", "|  No snippet with ID: 5")
        );
        String[] codes = new String[] {
                "int a = 0;", // var
                "class A {}", // class
                "void f() {}", // method
                "bool b;", // active failed
                "void g() { h(); }", // active corralled
        };
        List<ReplTest> tests = new ArrayList<>();
        for (String s : codes) {
            tests.add((a) -> assertCommand(a, s, null));
        }
        // Test /1 through /5 -- assure references are correct
        for (int i = 0; i < codes.length; ++i) {
            final int finalI = i;
            Consumer<String> check = (s) -> {
                String[] ss = s.split("\n");
                assertEquals(codes[finalI], ss[0]);
                assertTrue(ss.length > 1, s);
            };
            tests.add((a) -> assertCommandCheckOutput(a, "/" + (finalI + 1), check));
        }
        // Test /-1 ... note that the snippets added by history must be stepped over
        for (int i = 0; i < codes.length; ++i) {
            final int finalI = i;
            Consumer<String> check = (s) -> {
                String[] ss = s.split("\n");
                assertEquals(codes[codes.length - finalI - 1], ss[0]);
                assertTrue(ss.length > 1, s);
            };
            tests.add((a) -> assertCommandCheckOutput(a, "/-" + (2 * finalI + 1), check));
        }
        tests.add((a) -> assertCommandCheckOutput(a, "/!", assertStartsWith("int a = 0;")));
        test(false, new String[]{"--no-startup"},
                tests.toArray(new ReplTest[tests.size()]));
    }

    @Test
    public void test8142447() {
        Function<String, BiFunction<String, Integer, ReplTest>> assertRerun = cmd -> (code, assertionCount) ->
                (a) -> assertCommandCheckOutput(a, cmd, s -> {
                            String[] ss = s.split("\n");
                            assertEquals(code, ss[0]);
                            loadVariable(a, "int", "assertionCount", Integer.toString(assertionCount), Integer.toString(assertionCount));
                        });
        ReplTest assertVariables = (a) -> assertCommandCheckOutput(a, "/v", assertVariables());

        Compiler compiler = new Compiler();
        Path startup = compiler.getPath("StartupFileOption/startup.txt");
        compiler.writeToFile(startup, "int assertionCount = 0;\n" + // id: s1
                "void add(int n) { assertionCount += n; }");
        test(new String[]{"--startup", startup.toString()},
                (a) -> assertCommand(a, "add(1)", ""), // id: 1
                (a) -> assertCommandCheckOutput(a, "add(ONE)", s -> assertEquals("|  Error:", s.split("\n")[0])), // id: e1
                (a) -> assertVariable(a, "int", "ONE", "1", "1"),
                assertRerun.apply("/1").apply("add(1)", 2), assertVariables,
                assertRerun.apply("/e1").apply("add(ONE)", 3), assertVariables,
                assertRerun.apply("/s1").apply("int assertionCount = 0;", 0), assertVariables
        );

        test(false, new String[] {"--no-startup"},
                (a) -> assertCommand(a, "/s1", "|  No snippet with ID: s1"),
                (a) -> assertCommand(a, "/1", "|  No snippet with ID: 1"),
                (a) -> assertCommand(a, "/e1", "|  No snippet with ID: e1")
        );
    }

    @Test
    public void testClasspathDirectory() {
        Compiler compiler = new Compiler();
        Path outDir = Paths.get("testClasspathDirectory");
        compiler.compile(outDir, "package pkg; public class A { public String toString() { return \"A\"; } }");
        Path classpath = compiler.getPath(outDir);
        test(
                (a) -> assertCommand(a, "/env --class-path " + classpath,
                        "|  Setting new options and restoring state."),
                (a) -> evaluateExpression(a, "pkg.A", "new pkg.A();", "A")
        );
        test(new String[] { "--class-path", classpath.toString() },
                (a) -> evaluateExpression(a, "pkg.A", "new pkg.A();", "A")
        );
    }

    @Test
    public void testEnvInStartUp() {
        Compiler compiler = new Compiler();
        Path outDir = Paths.get("testClasspathDirectory");
        compiler.compile(outDir, "package pkg; public class A { public String toString() { return \"A\"; } }");
        Path classpath = compiler.getPath(outDir);
        Path sup = compiler.getPath("startup.jsh");
        compiler.writeToFile(sup,
                "int xxx;\n" +
                "/env -class-path " + classpath + "\n" +
                "int aaa = 735;\n"
        );
        test(
                (a) -> assertCommand(a, "/set start -retain " + sup, ""),
                (a) -> assertCommand(a, "/reset",
                        "|  Resetting state."),
                (a) -> evaluateExpression(a, "pkg.A", "new pkg.A();", "A"),
                (a) -> assertCommand(a, "aaa", "aaa ==> 735")
        );
        test(
                (a) -> assertCommandOutputContains(a, "/env", "--class-path"),
                (a) -> assertCommandOutputContains(a, "xxx", "cannot find symbol", "variable xxx"),
                (a) -> evaluateExpression(a, "pkg.A", "new pkg.A();", "A"),
                (a) -> assertCommand(a, "aaa", "aaa ==> 735")
        );
    }

    private String makeSimpleJar() {
        Compiler compiler = new Compiler();
        Path outDir = Paths.get("testClasspathJar");
        compiler.compile(outDir, "package pkg; public class A { public String toString() { return \"A\"; } }");
        String jarName = "test.jar";
        compiler.jar(outDir, jarName, "pkg/A.class");
        return compiler.getPath(outDir).resolve(jarName).toString();
    }

    @Test
    public void testClasspathJar() {
        String jarPath = makeSimpleJar();
        test(
                (a) -> assertCommand(a, "/env --class-path " + jarPath,
                        "|  Setting new options and restoring state."),
                (a) -> evaluateExpression(a, "pkg.A", "new pkg.A();", "A")
        );
        test(new String[] { "--class-path", jarPath },
                (a) -> evaluateExpression(a, "pkg.A", "new pkg.A();", "A")
        );
    }

    @Test
    public void testClasspathUserHomeExpansion() {
        String jarPath = makeSimpleJar();
        String tilde = "~" + File.separator;
        test(
                (a) -> assertCommand(a, "/env --class-path " + tilde + "forblato",
                        "|  File '" + Paths.get(System.getProperty("user.home"), "forblato").toString()
                                + "' for '--class-path' is not found."),
                (a) -> assertCommand(a, "/env --class-path " + jarPath + File.pathSeparator
                                                            + tilde + "forblato",
                        "|  File '" + Paths.get(System.getProperty("user.home"), "forblato").toString()
                                + "' for '--class-path' is not found.")
        );
    }

    @Test
    public void testBadClasspath() {
        String jarPath = makeSimpleJar();
        Compiler compiler = new Compiler();
        Path t1 = compiler.getPath("whatever/thing.zip");
        compiler.writeToFile(t1, "");
        Path t2 = compiler.getPath("whatever/thing.jmod");
        compiler.writeToFile(t2, "");
        test(
                (a) -> assertCommand(a, "/env --class-path " + t1.toString(),
                        "|  Invalid '--class-path' argument: " + t1.toString()),
                (a) -> assertCommand(a, "/env --class-path " + jarPath + File.pathSeparator + t1.toString(),
                        "|  Invalid '--class-path' argument: " + t1.toString()),
                (a) -> assertCommand(a, "/env --class-path " + t2.toString(),
                        "|  Invalid '--class-path' argument: " + t2.toString())
        );
    }

    private String makeBadSourceJar() {
        Compiler compiler = new Compiler();
        Path outDir = Paths.get("testClasspathJar");
        Path src = compiler.getPath(outDir.resolve("pkg/A.java"));
        compiler.writeToFile(src, "package pkg; /** \u0086 */public class A { public String toString() { return \"A\"; } }");
        String jarName = "test.jar";
        compiler.jar(outDir, jarName, "pkg/A.java");
        return compiler.getPath(outDir).resolve(jarName).toString();
    }

    @Test
    public void testBadSourceJarClasspath() {
        String jarPath = makeBadSourceJar();
        test(
                (a) -> assertCommand(a, "/env --class-path " + jarPath,
                        "|  Setting new options and restoring state."),
                (a) -> assertCommandOutputStartsWith(a, "new pkg.A();",
                        "|  Error:\n"
                        + "|  cannot find symbol\n"
                        + "|    symbol:   class A")
        );
        test(new String[]{"--class-path", jarPath},
                (a) -> assertCommandOutputStartsWith(a, "new pkg.A();",
                        "|  Error:\n"
                        + "|  cannot find symbol\n"
                        + "|    symbol:   class A")
        );
    }

    @Test
    public void testModulePath() {
        Compiler compiler = new Compiler();
        Path modsDir = Paths.get("mods");
        Path outDir = Paths.get("mods", "org.astro");
        compiler.compile(outDir, "package org.astro; public class World { public static String name() { return \"world\"; } }");
        compiler.compile(outDir, "module org.astro { exports org.astro; }");
        Path modsPath = compiler.getPath(modsDir);
        test(new String[] { "--module-path", modsPath.toString(), "--add-modules", "org.astro" },
                (a) -> assertCommand(a, "import org.astro.World;", ""),
                (a) -> evaluateExpression(a, "String",
                        "String.format(\"Greetings %s!\", World.name());",
                        "\"Greetings world!\"")
        );
    }

    @Test
    public void testModulePathUserHomeExpansion() {
        String tilde = "~" + File.separatorChar;
        test(
                (a) -> assertCommand(a, "/env --module-path " + tilde + "snardugol",
                        "|  File '" + Paths.get(System.getProperty("user.home"), "snardugol").toString()
                                + "' for '--module-path' is not found.")
        );
    }

    @Test
    public void testBadModulePath() {
        Compiler compiler = new Compiler();
        Path t1 = compiler.getPath("whatever/thing.zip");
        compiler.writeToFile(t1, "");
        test(
                (a) -> assertCommand(a, "/env --module-path " + t1.toString(),
                        "|  Invalid '--module-path' argument: " + t1.toString())
        );
    }

    @Test
    public void testStartupFileOption() {
        Compiler compiler = new Compiler();
        Path startup = compiler.getPath("StartupFileOption/startup.txt");
        compiler.writeToFile(startup, "class A { public String toString() { return \"A\"; } }");
        test(new String[]{"--startup", startup.toString()},
                (a) -> evaluateExpression(a, "A", "new A()", "A")
        );
        test(new String[]{"--no-startup"},
                (a) -> assertCommandCheckOutput(a, "Pattern.compile(\"x+\")", assertStartsWith("|  Error:\n|  cannot find symbol"))
        );
        test(
                (a) -> assertCommand(a, "Pattern.compile(\"x+\")", "$1 ==> x+", "", null, "", "")
        );
    }

    @Test
    public void testLoadingFromArgs() {
        Compiler compiler = new Compiler();
        Path path = compiler.getPath("loading.repl");
        compiler.writeToFile(path, "int a = 10; double x = 20; double a = 10;");
        test(new String[] { path.toString() },
                (a) -> assertCommand(a, "x", "x ==> 20.0"),
                (a) -> assertCommand(a, "a", "a ==> 10.0")
        );
    }

    @Test
    public void testReset() {
        test(
                (a) -> assertReset(a, "/res"),
                (a) -> assertCommandCheckOutput(a, "/methods", assertMethods()),
                (a) -> assertVariable(a, "int", "x"),
                (a) -> assertCommandCheckOutput(a, "/vars", assertVariables()),
                (a) -> assertMethod(a, "void f() { }", "()void", "f"),
                (a) -> assertCommandCheckOutput(a, "/methods", assertMethods()),
                (a) -> assertClass(a, "class A { }", "class", "A"),
                (a) -> assertCommandCheckOutput(a, "/types", assertClasses()),
                (a) -> assertImport(a, "import java.util.stream.*;", "", "java.util.stream.*"),
                (a) -> assertCommandCheckOutput(a, "/imports", assertImports()),
                (a) -> assertReset(a, "/reset"),
                (a) -> assertCommandCheckOutput(a, "/vars", assertVariables()),
                (a) -> assertCommandCheckOutput(a, "/methods", assertMethods()),
                (a) -> assertCommandCheckOutput(a, "/types", assertClasses()),
                (a) -> assertCommandCheckOutput(a, "/imports", assertImports())
        );
    }

    @Test
    public void testOpen() {
        Compiler compiler = new Compiler();
        Path path = compiler.getPath("testOpen.repl");
        compiler.writeToFile(path,
                "int a = 10;\ndouble x = 20;\ndouble a = 10;\n" +
                        "class A { public String toString() { return \"A\"; } }\nimport java.util.stream.*;");
        for (String s : new String[]{"/o", "/open"}) {
            test(
                    (a) -> assertCommand(a, s + " " + path.toString(), ""),
                    (a) -> assertCommand(a, "a", "a ==> 10.0"),
                    (a) -> evaluateExpression(a, "A", "new A();", "A"),
                    (a) -> evaluateExpression(a, "long", "Stream.of(\"A\").count();", "1"),
                    (a) -> {
                        loadVariable(a, "double", "x", "20.0", "20.0");
                        loadVariable(a, "double", "a", "10.0", "10.0");
                        loadVariable(a, "A", "$7", "new A();", "A");
                        loadVariable(a, "long", "$8", "Stream.of(\"A\").count();", "1");
                        loadClass(a, "class A { public String toString() { return \"A\"; } }",
                                "class", "A");
                        loadImport(a, "import java.util.stream.*;", "", "java.util.stream.*");
                        assertCommandCheckOutput(a, "/types", assertClasses());
                    },
                    (a) -> assertCommandCheckOutput(a, "/methods", assertMethods()),
                    (a) -> assertCommandCheckOutput(a, "/vars", assertVariables()),
                    (a) -> assertCommandCheckOutput(a, "/imports", assertImports())
            );
            Path unknown = compiler.getPath("UNKNOWN.repl");
            test(
                    (a) -> assertCommand(a, s + " " + unknown,
                            "|  File '" + unknown + "' for '/open' is not found.")
            );
        }
    }

    @Test
    public void testOpenLocalFileUrl() {
        Compiler compiler = new Compiler();
        Path path = compiler.getPath("testOpen.repl");
        compiler.writeToFile(path, "int a = 10;int b = 20;int c = a + b;\n");
        for (String s : new String[]{"/o", "/open"}) {
            test(
                    (a) -> assertCommand(a, s + " " + path.toUri(), ""),
                    (a) -> assertCommand(a, "a", "a ==> 10"),
                    (a) -> assertCommand(a, "b", "b ==> 20"),
                    (a) -> assertCommand(a, "c", "c ==> 30")
            );
        }
    }

    @Test
    public void testOpenFileOverHttp() throws IOException {
        var script = "int a = 10;int b = 20;int c = a + b;";

        var localhostAddress = new InetSocketAddress(InetAddress.getLoopbackAddress().getHostAddress(), 0);
        var httpServer = HttpServer.create(localhostAddress, 0);
        try {
            httpServer.createContext("/script", exchange -> {
                exchange.sendResponseHeaders(200, script.length());
                try (var output = exchange.getResponseBody()) {
                    output.write(script.getBytes());
                }
            });
            httpServer.setExecutor(null);
            httpServer.start();

            var urlAddress = "http:/" + httpServer.getAddress().toString() + "/script";
            for (String s : new String[]{"/o", "/open"}) {
                test(
                        (a) -> assertCommand(a, s + " " + urlAddress, ""),
                        (a) -> assertCommand(a, "a", "a ==> 10"),
                        (a) -> assertCommand(a, "b", "b ==> 20"),
                        (a) -> assertCommand(a, "c", "c ==> 30")
                );
            }
            test(new String[] {urlAddress},
                 "File '" + urlAddress + "' for 'jshell' is not found.\n");
        } finally {
            httpServer.stop(0);
        }
    }

    @Test
    public void testOpenResource() {
        test(new String[]{"-R", "-Duser.language=en", "-R", "-Duser.country=US"},
                (a) -> assertCommand(a, "/open PRINTING", ""),
                (a) -> assertCommandOutputContains(a, "/list",
                        "void println", "System.out.printf"),
                (a) -> assertCommand(a, "printf(\"%4.2f\", Math.PI)",
                        "", "", null, "3.14", "")
        );
    }

    @Test
    public void testSave() throws IOException {
        Compiler compiler = new Compiler();
        Path path = compiler.getPath("testSave.repl");
        {
            List<String> list = Arrays.asList(
                    "int a;",
                    "class A { public String toString() { return \"A\"; } }"
            );
            test(
                    (a) -> assertVariable(a, "int", "a"),
                    (a) -> assertCommand(a, "()", null, null, null, "", ""),
                    (a) -> assertClass(a, "class A { public String toString() { return \"A\"; } }", "class", "A"),
                    (a) -> assertCommand(a, "/save " + path.toString(), "")
            );
            assertEquals(list, Files.readAllLines(path));
        }
        {
            List<String> output = new ArrayList<>();
            test(
                    (a) -> assertCommand(a, "int a;", null),
                    (a) -> assertCommand(a, "()", null, null, null, "", ""),
                    (a) -> assertClass(a, "class A { public String toString() { return \"A\"; } }", "class", "A"),
                    (a) -> assertCommandCheckOutput(a, "/list -all", (out) ->
                                    output.addAll(Stream.of(out.split("\n"))
                            .filter(str -> !str.isEmpty())
                            .map(str -> str.substring(str.indexOf(':') + 2))
                            .filter(str -> !str.startsWith("/"))
                            .collect(Collectors.toList()))),
                    (a) -> assertCommand(a, "/save -all " + path.toString(), "")
            );
            assertEquals(output, Files.readAllLines(path));
        }
        {
            List<String> output = new ArrayList<>();
            test(
                    (a) -> assertCommand(a, "int a;", null),
                    (a) -> assertCommand(a, "int b;", null),
                    (a) -> assertCommand(a, "int c;", null),
                    (a) -> assertClass(a, "class A { public String toString() { return \"A\"; } }", "class", "A"),
                    (a) -> assertCommandCheckOutput(a, "/list b c a A", (out) ->
                                    output.addAll(Stream.of(out.split("\n"))
                            .filter(str -> !str.isEmpty())
                            .map(str -> str.substring(str.indexOf(':') + 2))
                            .filter(str -> !str.startsWith("/"))
                            .collect(Collectors.toList()))),
                    (a) -> assertCommand(a, "/save 2-3 1 4 " + path.toString(), "")
            );
            assertEquals(output, Files.readAllLines(path));
        }
        {
            List<String> output = new ArrayList<>();
            test(
                    (a) -> assertVariable(a, "int", "a"),
                    (a) -> assertCommand(a, "()", null, null, null, "", ""),
                    (a) -> assertClass(a, "class A { public String toString() { return \"A\"; } }", "class", "A"),
                    (a) -> assertCommandCheckOutput(a, "/history", (out) ->
                                output.addAll(Stream.of(out.split("\n"))
                            .filter(str -> !str.isEmpty())
                            .collect(Collectors.toList()))),
                    (a) -> assertCommand(a, "/save -history " + path.toString(), "")
            );
            output.add("/save -history " + path.toString());
            assertEquals(output, Files.readAllLines(path));
        }
    }

    @Test
    public void testStartRetain() {
        Compiler compiler = new Compiler();
        Path startUpFile = compiler.getPath("startUp.txt");
        test(
                (a) -> assertVariable(a, "int", "a"),
                (a) -> assertVariable(a, "double", "b", "10", "10.0"),
                (a) -> assertMethod(a, "void f() {}", "()V", "f"),
                (a) -> assertImport(a, "import java.util.stream.*;", "", "java.util.stream.*"),
                (a) -> assertCommand(a, "/save " + startUpFile.toString(), null),
                (a) -> assertCommand(a, "/set start -retain " + startUpFile.toString(), null)
        );
        Path unknown = compiler.getPath("UNKNOWN");
        test(
                (a) -> assertCommandOutputStartsWith(a, "/set start -retain " + unknown.toString(),
                        "|  File '" + unknown + "' for '/set start' is not found.")
        );
        test(false, new String[0],
                (a) -> {
                    loadVariable(a, "int", "a");
                    loadVariable(a, "double", "b", "10.0", "10.0");
                    loadMethod(a, "void f() {}", "()void", "f");
                    loadImport(a, "import java.util.stream.*;", "", "java.util.stream.*");
                    assertCommandCheckOutput(a, "/types", assertClasses());
                },
                (a) -> assertCommandCheckOutput(a, "/vars", assertVariables()),
                (a) -> assertCommandCheckOutput(a, "/methods", assertMethods()),
                (a) -> assertCommandCheckOutput(a, "/imports", assertImports())
        );
    }

    @Test
    public void testStartSave() throws IOException {
        Compiler compiler = new Compiler();
        Path startSave = compiler.getPath("startSave.txt");
        test(a -> assertCommand(a, "/save -start " + startSave.toString(), null));
        List<String> lines = Files.lines(startSave)
                .filter(s -> !s.isEmpty())
                .collect(Collectors.toList());
        assertEquals(START_UP, lines);
    }

    @Test
    public void testConstrainedUpdates() {
        test(
                a -> assertClass(a, "class XYZZY { }", "class", "XYZZY"),
                a -> assertVariable(a, "XYZZY", "xyzzy"),
                a -> assertCommandCheckOutput(a, "import java.util.stream.*",
                        (out) -> assertTrue(out.trim().isEmpty(), "Expected no output, got: " + out))
        );
    }

    @Test
    public void testRemoteExit() {
        test(
                a -> assertVariable(a, "int", "x"),
                a -> assertCommandCheckOutput(a, "/vars", assertVariables()),
                a -> assertCommandOutputContains(a, "System.exit(5);", "terminated"),
                a -> assertCommandCheckOutput(a, "/vars", s ->
                        assertTrue(s.trim().isEmpty(), s)),
                a -> assertMethod(a, "void f() { }", "()void", "f"),
                a -> assertCommandCheckOutput(a, "/methods", assertMethods())
        );
    }

    @Test
    public void testFeedbackNegative() {
        test(a -> assertCommandCheckOutput(a, "/set feedback aaaa",
                assertStartsWith("|  Does not match any current feedback mode")));
    }

    @Test
    public void testFeedbackSilent() {
        for (String off : new String[]{"s", "silent"}) {
            test(
                    a -> assertCommand(a, "/set feedback " + off, ""),
                    a -> assertCommand(a, "int a", ""),
                    a -> assertCommand(a, "void f() {}", ""),
                    a -> assertCommandCheckOutput(a, "aaaa", assertStartsWith("|  Error:"))
            );
        }
    }

    @Test
    public void testFeedbackNormal() {
        Compiler compiler = new Compiler();
        Path testNormalFile = compiler.getPath("testConciseNormal");
        String[] sources = new String[] {"int a", "void f() {}", "class A {}", "a = 10"};
        String[] sources2 = new String[] {"int a //again", "void f() {int y = 4;}", "class A {} //again", "a = 10"};
        String[] output = new String[] {
                "a ==> 0",
                "|  created method f()",
                "|  created class A",
                "a ==> 10"
        };
        compiler.writeToFile(testNormalFile, sources2);
        for (String feedback : new String[]{"/set fe", "/set feedback"}) {
            for (String feedbackState : new String[]{"n", "normal"}) {
                test(
                        a -> assertCommand(a, feedback + " " + feedbackState, "|  Feedback mode: normal"),
                        a -> assertCommand(a, sources[0], output[0]),
                        a -> assertCommand(a, sources[1], output[1]),
                        a -> assertCommand(a, sources[2], output[2]),
                        a -> assertCommand(a, sources[3], output[3]),
                        a -> assertCommand(a, "/o " + testNormalFile.toString(), "")
                );
            }
        }
    }

    @Test
    public void testVarsWithNotActive() {
        test(
                a -> assertVariable(a, "Blath", "x"),
                a -> assertCommandOutputContains(a, "/var -all", "(not-active)")
        );
    }

    @Test
    public void testHistoryReference() {
        test(false, new String[]{"--no-startup"},
                a -> assertCommand(a, "System.err.println(99)", "", "", null, "", "99\n"),
                a -> assertCommand(a, "/exit", "")
        );
        test(false, new String[]{"--no-startup"},
                a -> assertCommand(a, "System.err.println(1)", "", "", null, "", "1\n"),
                a -> assertCommand(a, "System.err.println(2)", "", "", null, "", "2\n"),
                a -> assertCommand(a, "/-2", "System.err.println(1)", "", null, "", "1\n"),
                a -> assertCommand(a, "/history",
                                                    "/debug 0\n" +
                                                    "System.err.println(1)\n" +
                                                    "System.err.println(2)\n" +
                                                    "System.err.println(1)\n" +
                                                    "/history\n"),
                a -> assertCommand(a, "/history -all",
                                                    "/debug 0\n" +
                                                    "System.err.println(99)\n" +
                                                    "/exit\n" +
                                                    "/debug 0\n" +
                                                    "System.err.println(1)\n" +
                                                    "System.err.println(2)\n" +
                                                    "System.err.println(1)\n" +
                                                    "/history\n" +
                                                    "/history -all\n"),
                a -> assertCommand(a, "/-2", "System.err.println(2)", "", null, "", "2\n"),
                a -> assertCommand(a, "/!", "System.err.println(2)", "", null, "", "2\n"),
                a -> assertCommand(a, "/2", "System.err.println(2)", "", null, "", "2\n"),
                a -> assertCommand(a, "/1", "System.err.println(1)", "", null, "", "1\n")
        );
    }

    @Test
    public void testRerunIdRange() {
        Compiler compiler = new Compiler();
        Path startup = compiler.getPath("rangeStartup");
        String[] startupSources = new String[] {
            "boolean go = false",
            "void println(String s) { if (go) System.out.println(s); }",
            "void println(int i) { if (go) System.out.println(i); }",
            "println(\"s4\")",
            "println(\"s5\")",
            "println(\"s6\")"
        };
        String[] sources = new String[] {
            "frog",
            "go = true",
            "println(2)",
            "println(3)",
            "println(4)",
            "querty"
        };
        compiler.writeToFile(startup, startupSources);
        test(false, new String[]{"--startup", startup.toString()},
                a -> assertCommandOutputStartsWith(a, sources[0], "|  Error:"),
                a -> assertCommand(a, sources[1], "go ==> true", "", null, "", ""),
                a -> assertCommand(a, sources[2], "", "", null, "2\n", ""),
                a -> assertCommand(a, sources[3], "", "", null, "3\n", ""),
                a -> assertCommand(a, sources[4], "", "", null, "4\n", ""),
                a -> assertCommandOutputStartsWith(a, sources[5], "|  Error:"),
                a -> assertCommand(a, "/3", "println(3)", "", null, "3\n", ""),
                a -> assertCommand(a, "/s4", "println(\"s4\")", "", null, "s4\n", ""),
                a -> assertCommandOutputStartsWith(a, "/e1", "frog\n|  Error:"),
                a -> assertCommand(a, "/2-4",
                        "println(2)\nprintln(3)\nprintln(4)",
                        "", null, "2\n3\n4\n", ""),
                a -> assertCommand(a, "/s4-s6",
                        startupSources[3] + "\n" +startupSources[4] + "\n" +startupSources[5],
                        "", null, "s4\ns5\ns6\n", ""),
                a -> assertCommand(a, "/s4-4", null,
                        "", null, "s4\ns5\ns6\n2\n3\n4\n", ""),
                a -> assertCommandCheckOutput(a, "/e1-e2",
                        s -> {
                            assertTrue(s.trim().startsWith("frog\n|  Error:"),
                                    "Output: \'" + s + "' does not start with: " + "|  Error:");
                            assertTrue(s.trim().lastIndexOf("|  Error:") > 10,
                                    "Output: \'" + s + "' does not have second: " + "|  Error:");
                        }),
                a -> assertCommand(a, "/4  s4 2",
                        "println(4)\nprintln(\"s4\")\nprintln(2)",
                        "", null, "4\ns4\n2\n", ""),
                a -> assertCommand(a, "/s5 2-4 3",
                        "println(\"s5\")\nprintln(2)\nprintln(3)\nprintln(4)\nprintln(3)",
                        "", null, "s5\n2\n3\n4\n3\n", ""),
                a -> assertCommand(a, "/2 ff", "|  No such snippet: ff"),
                a -> assertCommand(a, "/4-2", "|  End of snippet range less than start: 4 - 2"),
                a -> assertCommand(a, "/s5-s3", "|  End of snippet range less than start: s5 - s3"),
                a -> assertCommand(a, "/4-s5", "|  End of snippet range less than start: 4 - s5")
        );
    }

    @Test // TODO 8158197
    @Disabled
    public void testHeadlessEditPad() {
        String prevHeadless = System.getProperty("java.awt.headless");
        try {
            System.setProperty("java.awt.headless", "true");
            test(
                (a) -> assertCommandOutputStartsWith(a, "/edit printf", "|  Cannot launch editor -- unexpected exception:")
            );
        } finally {
            System.setProperty("java.awt.headless", prevHeadless==null? "false" : prevHeadless);
        }
    }

    @Test
    public void testAddExports() {
        test(false, new String[]{"--no-startup"},
                a -> assertCommandOutputStartsWith(a, "import jdk.internal.misc.VM;", "|  Error:")
        );
        test(false, new String[]{"--no-startup",
                        "-R--add-exports", "-Rjava.base/jdk.internal.misc=ALL-UNNAMED",
                        "-C--add-exports", "-Cjava.base/jdk.internal.misc=ALL-UNNAMED"},
                a -> assertImport(a, "import jdk.internal.misc.VM;", "", "jdk.internal.misc.VM"),
                a -> assertCommand(a, "System.err.println(VM.isBooted())", "", "", null, "", "true\n")
        );
        test(false, new String[]{"--no-startup", "--add-exports", "java.base/jdk.internal.misc"},
                a -> assertImport(a, "import jdk.internal.misc.VM;", "", "jdk.internal.misc.VM"),
                a -> assertCommand(a, "System.err.println(VM.isBooted())", "", "", null, "", "true\n")
        );
    }

    @Test
    public void testRedeclareVariableNoInit() {
        test(
                a -> assertCommand(a, "Integer a;", "a ==> null"),
                a -> assertCommand(a, "a instanceof Integer;", "$2 ==> false"),
                a -> assertCommand(a, "a = 1;", "a ==> 1"),
                a -> assertCommand(a, "Integer a;", "a ==> null"),
                a -> assertCommand(a, "a instanceof Integer;", "$5 ==> false"),
                a -> assertCommand(a, "a", "a ==> null")
        );
     }

    @Test
    public void testWarningUnchecked() { //8223688
        test(false, new String[]{"--no-startup"},
                a -> assertCommand(a, "abstract class A<T> { A(T t){} }", "|  created class A"),
                a -> assertCommandCheckOutput(a, "new A(\"\") {}", s -> {
                            assertStartsWith("|  Warning:");
                            assertTrue(s.contains("unchecked call"));
                            assertFalse(s.contains("Exception"));
                        })
        );
    }

    @Test
    public void testIndent() { //8223688
        prefsMap.remove("INDENT");
        test(false, new String[]{"--no-startup"},
                a -> assertCommand(a, "/set indent", "|  /set indent 4"),
                a -> assertCommand(a, "/set indent 2", "|  Indent level set to: 2"),
                a -> assertCommand(a, "/set indent", "|  /set indent 2"),
                a -> assertCommand(a, "/set indent broken", "|  Invalid indent level: broken"),
                a -> assertCommandOutputContains(a, "/set", "|  /set indent 2")
        );
    }

    @Test
    public void testSystemExitStartUp() {
        Compiler compiler = new Compiler();
        Path startup = compiler.getPath("SystemExitStartUp/startup.txt");
        compiler.writeToFile(startup, "int i1 = 0;\n" +
                                      "System.exit(0);\n" +
                                      "int i2 = 0;\n");
        test(Locale.ROOT, true, new String[]{"--startup", startup.toString()},
                "State engine terminated.",
                (a) -> assertCommand(a, "i2", "i2 ==> 0"),
                (a) -> assertCommandOutputContains(a, "i1", "Error:", "variable i1")
        );
    }
}
